/*
* 授权服务主要是做 颁发令牌；资源服务做的是验证令牌
* 授权服务配置三大块：
*    既然要完成认证，首先得知道客户信息（clients）；
*    既然要颁发token，那得先定义token相关的endpoints，以及token的存取，以及客户端支持那些类型的token（endpoints）；
*    既然暴露指定的endpoints，那就对这些endpoints定义一些安全上的约束（security）。
* */
package cn.learn.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

import javax.sql.DataSource;

/**
 * 授权服务器配置，@EnableAuthorizationServer开启授权服务中心
 *
 * @author huangyezhan
 * @date 2020年2月17日22:07:32
 */
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    /**
     * 注入权限验证控制器 来支持 password grant type
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 注入userDetailsService，开启refresh_token需要用到
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 数据源
     */
    //@Autowired
    //private DataSource dataSource;


    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 设置保存token的方式，一共有五种，这里采用数据库的方式
     */
    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;


    /**
     * 用来配置客户端详情服务（ClientDetailsService），客户端详情信息在这里进行初始化
     * 这里一般有两种方式处理：一、通过内存读取获取；二、通过数据库来存储调取详情信息。
     * ClientDetailsService负责查找ClientDetails，
     * ClientDetails的重要属性：【表oauth_client_details】
     * clientId：（必须的）用来表示客户id
     * secret：客户端安全码
     * authorizedGrantTypes：此客户端可以使用的授权类型，默认为空
     * authorities：此客户端可以使用的权限（基于Spring Security authorities）
     *
     * @param clients 客户信息详情配置
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //从数据库里面获取信息
        //clients.jdbc(dataSource);
        clients.withClientDetails(clientDetailsService);

        /*clients.inMemory()
                .withClient("user_1")
                .secret(passwordEncoder.encode("123456"))
                //可以访问的资源列表
                .resourceIds("Order")
                //允许的类型总共有五种：authorization_code,password,client_credentials,refresh_token,implicit
                //四中是登录授权模式,refresh_token刷新token
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "refresh_token", "implicit")
                //授权访问
                .scopes("all")
                //为false表示要跳转授权确认页面，为true则直接发放令牌
                .autoApprove("false")
                //.redirectUris("http://localhost:9501/login") //单点登录时配置
                .redirectUris("http://www.baidu.com")
        //.accessTokenValiditySeconds(3600)
        //.refreshTokenValiditySeconds(864000)
        ;*/

    }


    /*
    * 授权类型Grant types：
    * authenticationManager：认证管理器，选择密码（password）授权类型
    *
    * userDetailsService：如果你设置了这个属性的话，那说明你有一个自己的UserDetailsService接口的实现，或者你可以把这个东西设置到全局上面去
    *       （例如 GlobalAuthenticationManagerConfigurer 这个配置对象），当你设置了这个之后，那么”refresh_token“即刷新令牌类型模式的流程中就会包含一个检查，
    *       用来保证这个账号是否仍然有效，假如说你禁用了这个账户的话。
    *
    * authorizationCodeService：这个属性用来设置授权码服务（即AuthorizationCodeServices的实例对象），主要用于authorization_code授权码类型模式
    *
    * implicitGranService：设置隐式授权模式，用来管理隐式授权模式的状态
    *
    * tokenGranter：当你设置了这个（即TokenGranter接口实现），那么授权就会交由你来完全掌控，并且忽略上面的这几个属性；
    *       这个一般用来拓展的，即标准的四种授权模式已经满足不了你的需求，才会考虑使用这个属性
    * */

    /**
     * 用来配置授权（authorization）以及令牌（token）的访问端点和令牌服务(token services)
     * AuthorizationServerEndpointsConfigurer其实是一个装载类，
     * 装载Endpoints所有相关的类配置（AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService）。
     *
     * @param endpoints 配置相关端点的策略
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        //密码模式需要
        endpoints.authenticationManager(authenticationManager)
                //授权登录模式验证策略
                .authorizationCodeServices(authorizationCodeServices)
                //授权登录模式确认密码，令牌刷新校验
                .userDetailsService(userDetailsService)
                //令牌管理服务
                .tokenServices(tokenService())
                //允许提交请求的类型
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
        ;

        //配置token存储方式【设置 AccessToken的存储介质tokenStore， 默认使用内存当做存储介质。】
        //endpoints.tokenStore(tokenStore);
        //自定义登录或者鉴权失败时的返回信息
        //endpoints.exceptionTranslator(webResponseExceptionTranslator);


    }

    /**
     * 令牌访问端点安全策略：
     * 安全检查流程,用来配置令牌端点（Token Endpoint）的安全与权限访问
     * AuthorizationServerSecurityConfigurer继承SecurityConfigurerAdapter;
     * 也就是一个 Spring Security安全配置提供给AuthorizationServer去配置AuthorizationServer的端点（/oauth/****）的安全访问规则、过滤器Filter
     * <p>
     * 默认过滤器：BasicAuthenticationFilter
     * 1、oauth_client_details表中clientSecret字段加密【ClientDetails属性secret】
     * 2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤，默认值：denyAll()
     * <p>
     * 对以下的几个端点进行权限配置：
     * /oauth/authorize：授权端点
     * /oauth/token：令牌端点
     * /oauth/confirm_access：用户确认授权提交端点
     * /oauth/error：授权服务错误信息端点
     * /oauth/check_token：用于资源服务访问的令牌解析端点
     * /oauth/token_key：提供公有密匙的端点，如果使用JWT令牌的话
     *
     * @param security 用来配置令牌端点(Token Endpoint)的安全约束.
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                //  /oauth/check_token 公开
                .tokenKeyAccess("permitAll()")
                //  /oauth/check_token 公开
                .checkTokenAccess("permitAll()")
                // 允许表单认证，申请令牌
                .allowFormAuthenticationForClients()
        ;
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 令牌管理服务
     * 令牌一般分三种：
     * InMemoryTokenStore内存；
     * JdbcTokenStore数据库；
     * JwtTokenStore全称JSON Web Token(JWT)
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        //客户端消息服务
        service.setClientDetailsService(clientDetailsService);
        //是否产生刷新令牌
        service.setSupportRefreshToken(true);
        //令牌存储策略
        service.setTokenStore(tokenStore);
        //令牌默认有效期限2小时
        service.setAccessTokenValiditySeconds(7200);
        //刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }

    /**
     * 设置授权验证码模式的授权码如何存取，暂时定义内存模式
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }


    /*@Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource){
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }*/

}
